package com.thunder.reader.parser;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;

import com.thunder.reader.FeedSettings;
import com.thunder.reader.ReaderBaseException;
import com.thunder.reader.data.Rss;
import com.thunder.reader.data.UrlReader;
import com.thunder.reader.html.HtmlContentExtractor;
import com.thunder.reader.provider.Feed;

public class RssPullParser {
    private final String TAG = FeedSettings.GLOBAL_TAG;
    private InputStream mInputStream;
    private ContentResolver mContentResolver;
    private final Uri mSourceUri;
    private int mSourceId;
    private int mGroupId;
    private boolean mAbortParsing;
    private int mNewArticleCount;
    
    public RssPullParser(ContentResolver cr, Uri sourceUri, InputStream is) {
        mContentResolver = cr;
        mSourceUri = sourceUri;
        mInputStream = is;
        mAbortParsing = false;
        mNewArticleCount = 0;
    }
    
    public synchronized void parse() throws ReaderBaseException, XmlPullParserException, IOException {
        assert(mInputStream == null);
        final Cursor cursor = mContentResolver.query(mSourceUri, new String[]{Feed.Source._ID, Feed.Source.GROUP_ID}, null, null, null);
        if (cursor != null && cursor.moveToFirst()) {
            mSourceId = cursor.getInt(0);
            mGroupId = cursor.getInt(1);
            cursor.close();
        } else {
            Log.e(TAG, "cursor is emtpy something must be wrong");
        }
        final XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
        factory.setNamespaceAware(true);
        final XmlPullParser parser = factory.newPullParser();
        
        parser.setInput(mInputStream, null);
        int eventType = parser.getEventType();
        if (eventType != XmlPullParser.START_DOCUMENT) {
            throw new ReaderBaseException("Not starting with 'start_document'");
        }
        eventType = parseRss(parser);
        if (!mAbortParsing && eventType != XmlPullParser.END_DOCUMENT) {
            throw new ReaderBaseException("not ending with 'end_document', do you finish parsing?");
        }
        if (mInputStream != null) {
            mInputStream.close();
        }
    }
    
    /**
     * Parsing the Xml document. Current type must be Start_Document.
     * After calling this, Parser is positioned at END_DOCUMENT.
     * @param parser
     * @return event end_document 
     * @throws XmlPullParserException 
     * @throws ReaderBaseException 
     * @throws IOException 
     */
    private int parseRss(XmlPullParser parser) throws XmlPullParserException, ReaderBaseException, IOException {
        int eventType = parser.getEventType();
        if (eventType != XmlPullParser.START_DOCUMENT) {
            throw new ReaderBaseException("not starting with 'start_document', is this a new document?");
        }
        Log.e(TAG, "starting document, are you aware of that!");
        eventType = parser.next();
        while (!mAbortParsing && eventType != XmlPullParser.END_DOCUMENT) {
            switch (eventType) {
            case XmlPullParser.START_TAG: {
//                Log.e(TAG, "start tag: '" + parser.getName() + "'");
                final String tagName = parser.getName();
                if (tagName.equals(Rss.TAG_RSS)) {
//                    Log.e(TAG, "starting an RSS feed <<");
                } else if (tagName.equals(Rss.TAG_CHANNEL)) {
//                    Log.e(TAG, "\tstarting an Channel <<");
                    parseChannel(parser);
                } 
                break;
            }
            case XmlPullParser.END_TAG: {
//                Log.e(TAG, "end tag: '" + parser.getName() + "'");
                final String tagName = parser.getName();
                if (tagName.equals(Rss.TAG_RSS)) {
//                    Log.e(TAG, ">> edning an RSS feed");
                } else if (tagName.equals(Rss.TAG_CHANNEL)) {
//                    Log.e(TAG, "\t>> ending an Channel");     
                }
                break;
            }
            default:
                break;
            }
            eventType = parser.next();
        }
        Log.e(TAG, "end of document, it is over");
        return parser.getEventType();
    }
    
    /**
     * Parse a channel. MUST be start tag of an channel, otherwise exception thrown.
     * @param XmlPullParser
     * After calling this function, parser is positioned at END_TAG of Channel.
     * return end tag of a channel
     * @throws XmlPullParserException 
     * @throws ReaderBaseException 
     * @throws IOException 
     */
    private int parseChannel(XmlPullParser parser) throws XmlPullParserException, ReaderBaseException, IOException {
        int eventType = parser.getEventType();
        final String tagName = parser.getName();
        if (eventType != XmlPullParser.START_TAG || !Rss.TAG_CHANNEL.equals(tagName)) {
            throw new ReaderBaseException("not start with 'start tag', is this a start of a channel?");
        }
//        Log.e(TAG, "\tstarting " + tagName);
        ContentValues sourceValues = new ContentValues();
        eventType = parser.nextTag();
        while (!mAbortParsing && eventType != XmlPullParser.END_TAG) {
            switch (eventType) {
            case XmlPullParser.START_TAG: {
                final String tag = parser.getName();
                if (tag.equals(Rss.TAG_IMAGE)) {
                    sourceValues.put(Feed.Source.IMAGE_URL, parseImage(parser));
                    mContentResolver.update(mSourceUri, sourceValues, null, null);
                } else if (tag.equals(Rss.TAG_ITEM)) {
                    // about to parse item, so update the meta info about this channel first
                    if (sourceValues != null) {
//                        Log.e(TAG, "parseChannel, update meta into source");
                        mContentResolver.update(mSourceUri, sourceValues, null, null);
                        sourceValues = null;
                    }
                    parseItem(parser);
                } else {
                    final String content = parser.nextText();
                    if (tag.equals(Rss.TAG_TITLE)) {
//                        Log.e(TAG, "source " + tag + ": [" + content + "]");
                        sourceValues.put(Feed.Source.TITLE, content);
                    } else if (tag.equals(Rss.TAG_PUBDATE)) {
//                        Log.e(TAG, "source " + tag + ": [" + content + "]");
                        sourceValues.put(Feed.Source.MODIFIED_DATE, content);
                    } else if (tag.equals(Rss.TAG_DESCRIPTION)) {
//                        Log.e(TAG, "source " + tag + ": [" + "...description..." + "]");
                        sourceValues.put(Feed.Source.DESCRIPTION, content);
                    } else if (tag.equals(Rss.TAG_LINK)) {
//                        Log.e(TAG, "source " + tag + ": [" + content + "]");
                        sourceValues.put(Feed.Source.URL, content);
                    }
                }
                eventType = parser.nextTag();
                break;
            }
            default:
                break;
            }
        }
//        Log.e(TAG, "\tending " + parser.getName());
        return parser.getEventType();
    }

    /**
     * Parse image in a channel.
     * Precondition: position must be at START_TAG and tag MUST be 'image'
     * Postcondition: position is END_TAG of '/image'
     * @throws IOException 
     * @throws XmlPullParserException 
     * @throws ReaderBaseException 
     * @return the url locates the image, or null on any errors
     */
    private String parseImage(XmlPullParser parser) throws XmlPullParserException, IOException, ReaderBaseException {
        int eventType = parser.getEventType();
        String tag = parser.getName();
        if (eventType != XmlPullParser.START_TAG || !Rss.TAG_IMAGE.equals(tag)) {
            throw new ReaderBaseException("not start with 'start tag', is this a start of an image?");
        }
//        Log.e(TAG, "\t\tstarting image " + tag);
        eventType = parser.nextTag();
        String imageUrl = null;
        while (eventType != XmlPullParser.END_TAG) {
            switch (eventType) {
            case XmlPullParser.START_TAG:
                tag = parser.getName();
                final String content = parser.nextText();
                if (tag.equals(Rss.TAG_URL)) {
                    Log.e(TAG, "image " + tag + ": [" + content + "]");
                    imageUrl = content;
                }
                eventType = parser.nextTag();
                break;
            default:
                break;
            }
        }
//        Log.e(TAG, "\t\tending image " + parser.getName());
        return imageUrl;
    }
    
    /**
     * Parse an item in a channel.
     * Precondition: position must be at START_TAG and tag MUST be 'item'
     * Postcondition: position is END_TAG of '/item'
     * @throws IOException 
     * @throws XmlPullParserException 
     * @throws ReaderBaseException 
     */
    private int parseItem(XmlPullParser parser) throws XmlPullParserException, IOException, ReaderBaseException {
        int eventType = parser.getEventType();
        String tag = parser.getName();
        if (eventType != XmlPullParser.START_TAG || !Rss.TAG_ITEM.equals(tag)) {
            throw new ReaderBaseException("not start with 'start tag', is this a start of an item?");
        }
        final ContentValues articleValues = new ContentValues();
        articleValues.put(Feed.Article.SOURCE_ID, mSourceId);
        articleValues.put(Feed.Article.GROUP_ID, mGroupId);
        articleValues.put(Feed.Article.READ, 0);
//        Log.e(TAG, "\t\tstarting item " + tag);
        eventType = parser.nextTag();
        while (eventType != XmlPullParser.END_TAG) {
            switch (eventType) {
            case XmlPullParser.START_TAG:
                tag = parser.getName();
                final String content = parser.nextText();
                if (tag.equals(Rss.TAG_TITLE)) {
                    articleValues.put(Feed.Article.TITLE, content);
                } else if (tag.equals(Rss.TAG_PUBDATE)) {
                    final long pubDate = parseDateString(content);
                    articleValues.put(Feed.Article.PUB_DATE, pubDate);
                } else if (tag.equals(Rss.TAG_DESCRIPTION)) {
                    final String article = HtmlContentExtractor.extractTextFromHtml(content);
                    articleValues.put(Feed.Article.DESCRIPTION, getSummary(article));
                    final String url = articleValues.getAsString(Feed.Article.ARTICLE_URL);
                    if (!TextUtils.isEmpty(url)) {
                        // TODO: how to implement reader mode
                        final String fullText = new UrlReader(constructArticleUrl(url)).getContentAsString();
                        final String articleText = HtmlContentExtractor.extractFromHtml(fullText);
                        articleValues.put(Feed.Article.CONTENT, articleText);
                        articleValues.put(Feed.Article.IMAGE_URL, extractImageUrl(articleText));
                    } else {
                        Log.e(TAG, "oops, url is empty, use XML data");
                        articleValues.put(Feed.Article.CONTENT, content);
                        articleValues.put(Feed.Article.IMAGE_URL, extractImageUrl(content));
                    }
//                    Log.e(TAG, "parse item description "+content);
                } else if (tag.equals(Rss.TAG_LINK)) {
                    articleValues.put(Feed.Article.ARTICLE_URL, content);
                }
                eventType = parser.nextTag();
                break;
            default:
                break;
            }
        }
        final Cursor article = mContentResolver.query(Feed.Article.CONTENT_URI, new String[]{Feed.Article._ID}, 
                Feed.Article.ARTICLE_URL + "= ?", new String[]{articleValues.getAsString(Feed.Article.ARTICLE_URL)}, 
                null);
        if (article != null && article.moveToFirst()) {
            // This article already exists, stop parsing
            // However, this assumes that RSS server sort the articles pub date in descending order
            mAbortParsing = true;
        } else {
            mContentResolver.insert(Feed.Article.CONTENT_URI, articleValues);
            mNewArticleCount++;
            if (mNewArticleCount >= FeedSettings.getArticleLimit()) {
                mAbortParsing = true;
            }
        }
        if (article != null) {
            article.close();
        }
//        Log.e(TAG, "\t\tending " + parser.getName());
        return parser.getEventType();
    }
    
    @SuppressWarnings("unused")
    private void getFullArticle(String url, ContentValues articleValues) {
	url = constructArticleUrl(url);
        URL u;
        HttpURLConnection conn = null;
        InputStream in = null;
        OutputStream out = null;
        try {
            u = new URL(url);
            conn = (HttpURLConnection) u.openConnection();
            conn.setDoInput(true);
            conn.setDoOutput(false);
            conn.setConnectTimeout(60 * 1000);
            in = conn.getInputStream();
            String filename = articleValues.getAsString(Feed.Article.TITLE);
            filename = filename.replaceAll(" |\\-|\\.|/", "");
            filename += ".html";
            out = new FileOutputStream("/sdcard/" + filename);
            byte[] buf = new byte[8912];
            int seg = 0;
            while ((seg = in.read(buf)) != -1) {
                out.write(buf, 0, seg);
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
            try {
                if (in != null) {
                    in.close();
                }
                if (out != null) {
                    out.close();
                }
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    private String constructArticleUrl(String url) {
        //	Log.e(TAG, "old url '" + url + "'");
        url = URLEncoder.encode(url);
        url = "http://viewtext.org/article?url=" + url + "&format=";
        //	Log.e(TAG, "new url '" + url + "'");
        return url;
    }
    
    /**
     * Extract the first image  url of the given HTML doc, if any exists.
     * @param html
     * @return the first image's url, or null if no image at all.
     */
    private String extractImageUrl(String html) {
        // TODO: how to test it is a valid image tag or just a resource of webpage.
        // 1. Not *.gif
        // 2. has *.gif, but also have file="....*.jgp"
        // 2. has width and height attributes
        String url = null;
        final Pattern image = Pattern.compile("<(img.*?)>");
        final Matcher mimage = image.matcher(html);
        while (mimage.find()) {
            final String img = mimage.group(1);
            Log.e(TAG, "extract img raw img: " + img);
            if (!img.contains(".gif") || 
                    (img.contains(".gif") && img.contains("file=")) || 
                    (img.contains("width=") || img.contains("height="))) {
                // It is an image
                final Pattern asrc = Pattern.compile("img.*?src=\"(.+?)\"");
                final Matcher masrc = asrc.matcher(img);
                if (masrc.find()) {
                    url = masrc.group(1);
                }
                final Pattern afile = Pattern.compile("img.*?file=\"(.+?)\"");
                final Matcher mafile = afile.matcher(img);
                if (mafile.find()) {
                    url = mafile.group(1);
                }
                break;
            }
        }
        Log.e(TAG, "extract image url: " + url);
        return url;
    }
    
    /*
     * TODO: this method should be smart enough to recognize the word, 
     * in order to not show half of a English word.
     */
    private String getSummary(String description) {
        int limit = FeedSettings.getSummaryLimit();
        if (limit > description.length()) {
            limit = description.length();
        }
        return description.replaceAll("\\s+?", " ").substring(0, limit) + "...";
    }
    
    public synchronized void terminateParsing() {
        mAbortParsing = true;
    }
    
    /**
     * The seconds since 1970-1-1, in seconds
     * @param date
     * @return
     */
    private long parseDateString(String date) {
        if (TextUtils.isEmpty(date)) {
            return System.currentTimeMillis();
        }
        // try all possible formats
        final String[] formats = new String[] {
                "EEE, dd MMM yyyy HH:mm:ss Z",
                "yyyy-MM-dd'T'HH:mm:ss'Z'",
                "yyyy-MM-dd HH:mm:ss",
        };
        Date d = new Date();
        for (String f : formats) {
            SimpleDateFormat sdf = new SimpleDateFormat(f);
            try {
                d = sdf.parse(date);
                break;
            } catch (ParseException e) {
            }
        }
//        Log.e(TAG, "date date is " + d);
        return d.getTime();
    }
}